package com.github.lh

import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
import java.util.*


object Chain {
    private val blockchain: ArrayList<Block> = arrayListOf()
    val minimumTransaction = 0.1f
    val difficulty = 1
    val UTXOs: HashMap<String, TransactionOutput> = hashMapOf()

    val coinbase = Wallet("CB")
    val firstBlock = Block("0")
    lateinit var genesisTransaction: Transaction

    fun initBaseCoinCount(balance: Float) {
        genesisTransaction = Transaction(coinbase.publicKey, coinbase.publicKey, balance, arrayListOf())
        genesisTransaction.generateSignature(coinbase.privateKey)
        genesisTransaction.transactionId = "0"
        genesisTransaction.outputs.add(TransactionOutput(genesisTransaction.receiver, genesisTransaction.value, genesisTransaction.transactionId))
        Chain.UTXOs[genesisTransaction.outputs.first().id] = genesisTransaction.outputs.first()
        if (firstBlock.addTransaction(genesisTransaction)) addBlock(firstBlock)
        printBalance(coinbase)
    }

    fun isChainValid(): Boolean {
        var currentBlock: Block
        var previousBlock: Block
        val hashTarget = String(CharArray(difficulty)).replace('\u0000', '0')
        val tempUTXOs = hashMapOf<String, TransactionOutput>() //a temporary working list of unspent transactions at a given block state.
        tempUTXOs[genesisTransaction.outputs.first().id] = genesisTransaction.outputs.first()

        //loop through blockchain to check hashes:
        for (i in 1 until blockchain.size) {

            currentBlock = blockchain.get(i)
            previousBlock = blockchain.get(i - 1)
            //compare registered hash and calculated hash:
            if (currentBlock.hash != currentBlock.calculateHash()) {
                println("#Current Hashes not equal")
                return false
            }
            //compare previous hash and registered previous hash
            if (previousBlock.hash != currentBlock.previousHash) {
                println("#Previous Hashes not equal")
                return false
            }
            //check if hash is solved
            if (currentBlock.hash.substring(0, difficulty) != hashTarget) {
                println("#This block hasn't been mined")
                return false
            }

            //loop thru blockchains transactions:
            var tempOutput: TransactionOutput?
            for (t in currentBlock.transactions.indices) {
                val currentTransaction = currentBlock.transactions[t]

                if (!currentTransaction.verifySignature()) {
                    println("#Signature on Transaction($t) is Invalid")
                    return false
                }
                if (currentTransaction.getInputsValue() != currentTransaction.getOutputsValue()) {
                    println("#Inputs are note equal to outputs on Transaction($t)")
                    return false
                }

                for (input in currentTransaction.inputs) {
                    tempOutput = tempUTXOs[input.transactionOutputId]

                    if (tempOutput == null) {
                        println("#Referenced input on Transaction($t) is Missing")
                        return false
                    }

                    if (input.UTXO!!.value != tempOutput.value) {
                        println("#Referenced input Transaction($t) value is Invalid")
                        return false
                    }

                    tempUTXOs.remove(input.transactionOutputId)
                }

                for (output in currentTransaction.outputs) {
                    tempUTXOs[output.id] = output
                }

                if (currentTransaction.outputs[0].receiver !== currentTransaction.receiver) {
                    println("#Transaction($t) output reciepient is not who it should be")
                    return false
                }
                if (currentTransaction.outputs[1].receiver !== currentTransaction.sender) {
                    println("#Transaction($t) output 'change' is not sender.")
                    return false
                }

            }

        }
        println("Blockchain is valid")
        return true
    }

    fun addBlock(newBlock: Block) {
        newBlock.mineBlock(difficulty)
        blockchain.add(newBlock)
        printChain()
    }

    fun printChain() {
        println(JSON.toJSONString(blockchain, SerializerFeature.PrettyFormat))
        println("Chain length: ${blockchain.size}")
    }

    fun blockTransaction(sender: Wallet, receiver: Wallet, balance: Float) {
        val b1 = Block(blockchain.last().hash)
        if (b1.addTransaction(sender.sendFunds(receiver.publicKey, balance)))
            Chain.addBlock(b1)
        printBalance(sender, receiver, coinbase)
    }
}

fun printBalance(vararg wallets: Wallet) {
    wallets.forEach { it.printBalance() }
}



fun main(args: Array<String>) {
    Security.addProvider(BouncyCastleProvider()) //Setup Bouncey castle as a Security Provider
    Chain.run {
        initBaseCoinCount(1000f)

        val walletA = Wallet("A")
        val walletB = Wallet("B")
        printBalance(walletA, walletB)


        blockTransaction(coinbase, walletA, 30f)

        blockTransaction(walletA, walletB, 40f)

        blockTransaction(walletB, walletA, 0f)

        blockTransaction(walletA, walletB, 10f)

        blockTransaction(walletA, walletB, 30f)

        blockTransaction(walletA, walletB, 20f)

        blockTransaction(coinbase, walletA, 100f)

        isChainValid()
    }
}
